/*
 *  Copyright 2011 derek.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.aocore.cdkey;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 *
 * @author derek
 */
public class CdKey
{
    String key;
    boolean valid;
    int productId;
    int publicValue;
    int privateValue;

    public int getPublicValue()
    {
        return publicValue;
    }

    static final char[] Map =
    {
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0x00, 0xff, 0x01, 0xff, 0x02, 0x03, 0x04, 0x05, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
        0x0c, 0xff, 0x0d, 0x0e, 0xff, 0x0f, 0x10, 0xff, 0x11, 0xff, 0x12, 0xff,
        0x13, 0xff, 0x14, 0x15, 0x16, 0xff, 0x17, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0xff, 0x0d, 0x0e,
        0xff, 0x0f, 0x10, 0xff, 0x11, 0xff, 0x12, 0xff, 0x13, 0xff, 0x14, 0x15,
        0x16, 0xff, 0x17, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff
    };

    public CdKey(String key)
    {
        this.key = key;
        char buffer[] = key.toCharArray();

        int r = 1;
        int checksum = 0;
        int n1 = 0;
        int n2 = 0;
        int v1 = 0;
        int v2 = 0;
        char c1 = 0;
        char c2 = 0;

        for (int i = 0; i < 16; i += 2)
        {
            c1 = Map[key.charAt(i)];
            n1 = c1 * 3;

            c2 = Map[buffer[i + 1]];
            n1 = c2 + n1 * 8;

            if (n1 >= 0x100)
            {
                n1 -= 0x100;
                checksum |= r;
            }

            n2 = n1 >>> 4;

            buffer[i] = valueToHex(n2 % 16);
            buffer[i + 1] = valueToHex(n1 % 16);

            r <<= 1;
        }

        v1 = 0x03;
        for (int i = 0; i < 16; i++)
        {
            c1 = (char) (buffer[i] & 0xff);
            n1 = (char) (hexToValue(c1));
            n2 = v1 * 2;
            n1 ^= n2;
            v1 += n1;
        }
        v1 &= 0xff;

        if (v1 != checksum)
        {
            productId = -1;
            publicValue = -1;
            privateValue = -1;
            return;
        }

        n1 = 0;
        for (int i = 15; i >= 0; i--)
        {
            c1 = buffer[i];
            if (i > 8)
            {
                n1 = i - 9;
            }
            else
            {
                n1 = 0x0f + i - 8;
            }

            n1 &= 0x0f;
            c2 = buffer[n1];
            buffer[i] = c2;
            buffer[n1] = c1;
        }

        v2 = 0x13ac9741;
        for (int i = 15; i >= 0; i--)
        {
            buffer[i] = Character.toUpperCase(buffer[i]);
            c1 = buffer[i];

            if (c1 <= '7')
            {
                v1 = v2;
                c2 = (char) (((v1 & 0xff) & 7) ^ c1);
                v1 >>>= 3;
                buffer[i] = c2;
                v2 = v1;
            }
            else if (c1 < 'A')
            {
                buffer[i] = (char) ((i & 1) ^ c1);
            }
        }

        productId = hexToValue(buffer, 0, 2);
        publicValue = hexToValue(buffer, 2, 6);
        privateValue = hexToValue(buffer, 8, 8);
        valid = true;
    }

    public byte[] computeHash(int clientToken, int serverToken)
    {
        byte[] buffer = new byte[1024];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

        byteBuffer.putInt(0, clientToken);
        byteBuffer.putInt(4, serverToken);
        byteBuffer.putInt(8, productId);
        byteBuffer.putInt(12, publicValue);
        byteBuffer.putInt(16, 0);
        byteBuffer.putInt(20, privateValue);

        for (int i = 0; i < 64; i++)
        {
            int shift = (byteBuffer.getInt(i*4) ^ byteBuffer.getInt((i + 8)*4) ^ byteBuffer.getInt((i + 2)*4) ^ byteBuffer.getInt((i + 13)*4)) % 32;
            byteBuffer.putInt((i + 16)*4, rotateLeft(1, shift));
        }

        int a = 0x67452301;
        int b = 0xefcdab89;
        int c = 0x98badcfe;
        int d = 0x10325476;
        int e = 0xc3d2e1f0;
        int g = 0;

        for (int i = 0; i < 20; i++)
        {
            g = byteBuffer.getInt() + rotateLeft(a, 5) + e + ((b & c) | (~b & d)) + 0x5a827999;
            e = d;
            d = c;
            c = rotateLeft(b, 30);
            b = a;
            a = g;
        }

        for (int i = 0; i < 20; i++)
        {
            g = (d ^ c ^ b) + e + rotateLeft(g, 5) + byteBuffer.getInt() + 0x6ed9eba1;
            e = d;
            d = c;
            c = rotateLeft(b, 30);
            b = a;
            a = g;
        }

        for (int i = 0; i < 20; i++)
        {
            g = byteBuffer.getInt() + rotateLeft(g, 5) + e + ((c & b) | (d & c) | (d & b)) - 0x70e44324;
            e = d;
            d = c;
            c = rotateLeft(b, 30);
            b = a;
            a = g;
        }

        for (int i = 0; i < 20; i++)
        {
            g = (d ^ c ^ b) + e + rotateLeft(g, 5) + byteBuffer.getInt() - 0x359d3e2a;
            e = d;
            d = c;
            c = rotateLeft(b, 30);
            b = a;
            a = g;
        }

        byte[] result = new byte[20];
        byteBuffer = ByteBuffer.wrap(result);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);

        byteBuffer.putInt(0x67452301 + a);
        byteBuffer.putInt(0xefcdab89 + b);
        byteBuffer.putInt(0x98badcfe + c);
        byteBuffer.putInt(0x10325476 + d);
        byteBuffer.putInt(0xc3d2e1f0 + e);

        return result;
    }

    private int rotateLeft(int value, int shift)
    {
        shift &= 0x1f;
        value = (value >>> (0x20 - shift)) | (value << shift);
        return value;
    }

    private int hexToValue(char c)
    {
        c = Character.toLowerCase(c);

        if (c >= '0' && c <= '9')
        {
            return c - '0';
        }

        if (c >= 'a' && c <= 'f')
        {
            return c - 'a' + 0x0a;
        }

        return -1;
    }

    private char valueToHex(int v)
    {
        if (v >= 0 && v <= 9)
        {
            return (char) ('0' + v);
        }

        if (v >= 0x0a && v <= 0x0f)
        {
            return (char) ('a' + v - 0x0a);
        }

        return (char) -1;
    }

    private int hexToValue(char[] s, int offset, int n)
    {
        int v = 0;

        for (int i = 0; i < n; i++)
        {
            v |= hexToValue(s[offset + i]) << ((n - i - 1) * 4);
        }

        return v;
    }

    @Override
    public String toString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("key: ").append(key).append("\n");
        stringBuilder.append("valid: ").append(valid).append("\n");
        stringBuilder.append("productId: ").append(productId).append("\n");
        stringBuilder.append("publicValue: ").append(publicValue).append("\n");
        stringBuilder.append("privateValue: ").append(privateValue).append("\n");
        return stringBuilder.toString();
    }
}
